/*-
 * Copyright (c) 2008-2010, Oleg Estekhin
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the distribution.
 *  * Neither the names of the copyright holders nor the names of their
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 */

package oe.assertions.predicates;

import oe.assertions.MessageBuffer;
import oe.assertions.Predicate;

/** Evaluates to {@code true} if the input value is close to the target value. */
public class IsCloseTo<T extends Number> extends BinaryNumberPredicate<T> {

    private final Difference difference;


    /**
     * Creates a new predicate.
     *
     * @param target the target value
     */
    public IsCloseTo( T target ) {
        this( target, new NoDifference() );
    }

    /**
     * Creates a new predicate with the specified difference algorithm.
     *
     * @param target the target value
     * @param difference the difference algorithm
     */
    private IsCloseTo( T target, Difference difference ) {
        super( target );
        if ( difference == null ) {
            throw new IllegalArgumentException( "difference is null" );
        }
        this.difference = difference;
    }


    @Override
    protected boolean checkDouble( double input, double target ) {
        return difference.checkDouble( input, target );
    }

    @Override
    protected boolean checkFloat( float input, float target ) {
        return difference.checkFloat( input, target );
    }

    @Override
    protected boolean checkNumber( Number input, Number target ) {
        return difference.checkNumber( input, target );
    }

    @Override
    protected boolean checkObject( Object input, Object target ) {
        return false;
    }

    @Override
    protected void describe( MessageBuffer message ) {
        message.indent().append( "to be close to" ).append( ' ' ).formatValue( target );
        difference.describe( message );
        message.eol();
    }


    /**
     * Returns a predicate that evaluates to {@code true} if the input value is close to the target value using the ulp
     * distance between their floating-point representations.
     *
     * @param tolerance the tolerance
     *
     * @return a new predicate
     *
     * @throws IllegalArgumentException if {@code tolerance} is negative
     */
    public Predicate<T> byUlpDifference( long tolerance ) {
        return new IsCloseTo<T>( target, new UlpDifference( tolerance ) );
    }

    /**
     * Returns a predicate that evaluates to {@code true} if the input value is close to the target value using the
     * absolute difference between them, that is if {@code |input - target| <= tolerance}.
     *
     * @param tolerance the tolerance
     *
     * @return a new predicate
     *
     * @throws IllegalArgumentException if {@code tolerance} is {@code null}
     * @throws IllegalArgumentException if {@code tolerance} is negative or not a finite number
     */
    public Predicate<T> byAbsoluteDifference( T tolerance ) {
        return new IsCloseTo<T>( target, new AbsoluteDifference<T>( tolerance ) );
    }

    /**
     * Returns a predicate that evaluates to {@code true} if the input value is close to the target value using the
     * relative difference between them, that is if either {@code |(input - target) / input| <= tolerance} or {@code
     * |(input - target) / target| <= tolerance}.
     *
     * @param tolerance the tolerance
     *
     * @return a new predicate
     *
     * @throws IllegalArgumentException if {@code tolerance} is negative or not a finite number
     */
    public Predicate<T> byRelativeDifference( double tolerance ) {
        return new IsCloseTo<T>( target, new RelativeDifference( tolerance ) );
    }

    /**
     * Returns a predicate that evaluates to {@code true} if the input value is close to the target value using the
     * strict relative difference between them, that is if both {@code |(input - target) / input| <= tolerance} and
     * {@code |(input - target) / target| <= tolerance}.
     *
     * @param tolerance the tolerance
     *
     * @return a new predicate
     *
     * @throws IllegalArgumentException if {@code tolerance} is negative or not a finite number
     */
    public Predicate<T> byStrictRelativeDifference( double tolerance ) {
        return new IsCloseTo<T>( target, new StrictRelativeDifference( tolerance ) );
    }


    private interface Difference {

        boolean checkDouble( double input, double target );

        boolean checkFloat( float input, float target );

        boolean checkNumber( Number input, Number target );

        void describe( MessageBuffer message );

    }

    private static class NoDifference implements Difference {

        @Override
        public boolean checkDouble( double input, double target ) {
            return !Double.isNaN( input ) && !Double.isInfinite( input ) && Double.compare( input, target ) == 0;
        }

        @Override
        public boolean checkFloat( float input, float target ) {
            return !Float.isNaN( input ) && !Float.isInfinite( input ) && Float.compare( input, target ) == 0;
        }

        @Override
        public boolean checkNumber( Number input, Number target ) {
            return input.equals( target );
        }

        @Override
        public void describe( MessageBuffer message ) {
        }

    }

    private static class UlpDifference implements Difference {

        private final long tolerance;

        private UlpDifference( long tolerance ) {
            if ( tolerance < 0L ) {
                throw new IllegalArgumentException( "tolerance is negative" );
            }
            this.tolerance = tolerance;
        }

        @Override
        public boolean checkDouble( double input, double target ) {
            if ( Double.isNaN( input ) || Double.isInfinite( input )
                    || Double.isNaN( target ) || Double.isInfinite( target ) ) {
                return false;
            } else {
                return checkLong( Double.doubleToLongBits( input ), Double.doubleToLongBits( target ) );
            }
        }

        @Override
        public boolean checkFloat( float input, float target ) {
            if ( Float.isNaN( input ) || Float.isInfinite( input )
                    || Float.isNaN( target ) || Float.isInfinite( target ) ) {
                return false;
            } else {
                return checkInt( Float.floatToIntBits( input ), Float.floatToIntBits( target ) );
            }
        }

        @Override
        public boolean checkNumber( Number input, Number target ) {
            return checkLong( input.longValue(), target.longValue() );
        }

        private boolean checkLong( long input, long target ) {
            long difference;
            if ( target <= 0L ) {
                difference = Long.MAX_VALUE + target >= input ? input - target : Long.MIN_VALUE;
            } else {
                difference = Long.MIN_VALUE + target <= input ? input - target : Long.MIN_VALUE;
            }
            return -Math.abs( difference ) >= -tolerance;
        }

        private boolean checkInt( int input, int target ) {
            int difference;
            if ( target <= 0 ) {
                difference = Integer.MAX_VALUE + target >= input ? input - target : Integer.MIN_VALUE;
            } else {
                difference = Integer.MIN_VALUE + target <= input ? input - target : Integer.MIN_VALUE;
            }
            return -Math.abs( difference ) >= -tolerance;
        }

        @Override
        public void describe( MessageBuffer message ) {
            //message.indent().append( "to be close to" ).append( ' ' ).formatValue( target );
            message.append( ' ' ).append( "by the ulp difference no more than" ).append( ' ' ).format( "%s", tolerance );
            //message.eol();
        }

    }

    private static class AbsoluteDifference<T extends Number> implements Difference {

        private final T tolerance;

        private AbsoluteDifference( T tolerance ) {
            if ( tolerance == null ) {
                throw new IllegalArgumentException( "tolerance is null" );
            }
            if ( tolerance instanceof Double || tolerance instanceof Float ) {
                double d = tolerance.doubleValue();
                if ( Double.isNaN( d ) || Double.isInfinite( d ) ) {
                    throw new IllegalArgumentException( "tolerance is not a finite number" );
                }
                if ( Double.compare( d, 0.0 ) < 0 ) {
                    throw new IllegalArgumentException( "tolerance is negative" );
                }
            } else if ( tolerance instanceof Number ) {
                long l = tolerance.longValue();
                if ( l < 0L ) {
                    throw new IllegalArgumentException( "tolerance is negative" );
                }
            } else {
                throw new AssertionError( tolerance.getClass() );
            }
            this.tolerance = tolerance;
        }

        @Override
        public boolean checkDouble( double input, double target ) {
            return Math.abs( input - target ) <= tolerance.doubleValue();
        }

        @Override
        public boolean checkFloat( float input, float target ) {
            return Math.abs( input - target ) <= tolerance.floatValue();
        }

        @Override
        public boolean checkNumber( Number input, Number target ) {
            long inputAsLong = input.longValue();
            long targetAsLong = target.longValue();
            long difference;
            if ( targetAsLong <= 0L ) {
                difference = Long.MAX_VALUE + targetAsLong >= inputAsLong ? inputAsLong - targetAsLong : Long.MIN_VALUE;
            } else {
                difference = Long.MIN_VALUE + targetAsLong <= inputAsLong ? inputAsLong - targetAsLong : Long.MIN_VALUE;
            }
            return -Math.abs( difference ) >= -tolerance.longValue();
        }

        @Override
        public void describe( MessageBuffer message ) {
            //message.indent().append( "to be close to" ).append( ' ' ).formatValue( target );
            message.append( ' ' ).append( "by the absolute difference no more than" ).append( ' ' ).format( "%s", tolerance );
            //message.eol();
        }

    }

    private static class RelativeDifference implements Difference {

        private final double tolerance;

        private RelativeDifference( double tolerance ) {
            if ( Double.isNaN( tolerance ) || Double.isInfinite( tolerance ) ) {
                throw new IllegalArgumentException( "tolerance is not a finite number" );
            }
            if ( Double.compare( tolerance, 0.0 ) < 0 ) {
                throw new IllegalArgumentException( "tolerance is negative" );
            }
            this.tolerance = tolerance;
        }

        @Override
        public boolean checkDouble( double input, double target ) {
            return Math.abs( input - target ) <= Math.abs( tolerance * input )
                    || Math.abs( input - target ) <= Math.abs( tolerance * target );
        }

        @Override
        public boolean checkFloat( float input, float target ) {
            return checkDouble( input, target );
        }

        @Override
        public boolean checkNumber( Number input, Number target ) {
            return checkDouble( input.doubleValue(), target.doubleValue() );
        }

        @Override
        public void describe( MessageBuffer message ) {
            //message.indent().append( "to be close to" ).append( ' ' ).formatValue( target );
            message.append( ' ' ).append( "by the relative difference no more than" ).append( ' ' ).format( "%s", tolerance );
            //message.eol();
        }

    }

    private static class StrictRelativeDifference implements Difference {

        private final double tolerance;

        private StrictRelativeDifference( double tolerance ) {
            if ( Double.isNaN( tolerance ) || Double.isInfinite( tolerance ) ) {
                throw new IllegalArgumentException( "tolerance is not a finite number" );
            }
            if ( Double.compare( tolerance, 0.0 ) < 0 ) {
                throw new IllegalArgumentException( "tolerance is negative" );
            }
            this.tolerance = tolerance;
        }

        @Override
        public boolean checkDouble( double input, double target ) {
            return Math.abs( input - target ) <= Math.abs( tolerance * input )
                    && Math.abs( input - target ) <= Math.abs( tolerance * target );
        }

        @Override
        public boolean checkFloat( float input, float target ) {
            return checkDouble( input, target );
        }

        @Override
        public boolean checkNumber( Number input, Number target ) {
            return checkDouble( input.doubleValue(), target.doubleValue() );
        }

        @Override
        public void describe( MessageBuffer message ) {
            //message.indent().append( "to be close to" ).append( ' ' ).formatValue( target );
            message.append( ' ' ).append( "by the strict relative difference no more than" ).append( ' ' ).format( "%s", tolerance );
            //message.eol();
        }

    }

}
